
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: CloseableNotes.java 380 2010-03-11 22:27:58Z archie.cobbs $
 */

package org.dellroad.sidekar.util;

import java.util.LinkedList;
import java.util.concurrent.Callable;

import org.dellroad.stuff.schema.DatabaseAction;

/**
 * Class that maintains a set of {@link Closeable} objects that need to be closed.
 *
 * <p>
 * Typical use is via {@link #performAction performAction()}, which automatically creates an instance of this class
 * and associates it with the current thread for the duration of the given {@link DatabaseAction} callback.
 * The convenience method {@link org.dellroad.sidekar.Database#performAction Database.performAction()}
 * should be used for operations associated with a {@link org.dellroad.sidekar.Database}.
 * </p>
 *
 * <p>
 * This class is thread safe.
 * </p>
 *
 * @see org.dellroad.sidekar.Database#performAction Database.performAction()
 * @see org.dellroad.sidekar.Database#performTransaction Database.performTransaction()
 */
public class CloseableNotes {

    private static final ThreadLocal<LinkedList<CloseableNotes>> THREAD_NOTES = new ThreadLocal<LinkedList<CloseableNotes>>();

    private final LinkedList<Closeable> closeables = new LinkedList<Closeable>();

    /**
     * Register a {@link Closeable} with this instance.
     *
     * @throws IllegalArgumentException if closeable is null
     */
    public void add(Closeable closeable) {
        if (closeable == null)
            throw new IllegalArgumentException("null closeable");
        synchronized (this.closeables) {
            this.closeables.push(closeable);
        }
    }

    /**
     * Close all {@link Closeable}s registered with this instance.
     */
    public void closeAll() {
        while (true) {
            Closeable closeable;
            synchronized (this.closeables) {
                if (this.closeables.isEmpty())
                    break;
                closeable = this.closeables.pop();
            }
            closeable.close();
        }
    }

    /**
     * Perform some action and ensure that any {@link Closeable}s that register themselves
     * via {@link #addToCurrentAction addToCurrentAction()} will be closed just before this method returns.
     *
     * <p>
     * This method first invokes {@link #push}, then executes the given {@code action}, then
     * invokes {@link #pop} (no matter how {@code action} terminates).
     * </p>
     *
     * <p>
     * Normally this method is not invoked directly;
     * use {@link org.dellroad.sidekar.Database#performAction Database.performAction()}
     * instead.
     * </p>
     *
     * @see #addToCurrentAction
     * @see org.dellroad.sidekar.Database#performAction Database.performAction()
     */
    public static void performAction(Runnable action) {
        push();
        try {
            action.run();
        } finally {
            pop();
        }
    }

    public static <R> R performAction(Callable<R> action) throws Exception {
        push();
        try {
            return action.call();
        } finally {
            pop();
        }
    }

    /**
     * Associate a new instance with the current thread.
     *
     * <p>
     * Callers must ensure that a matching invocation of {@link #pop} occurs (exactly once) in this thread.
     */
    public static void push() {

        // Get the current instance
        if (THREAD_NOTES.get() == null)
            THREAD_NOTES.set(new LinkedList<CloseableNotes>());
        LinkedList<CloseableNotes> noteStack = THREAD_NOTES.get();

        // Add a new instance to the thread's stack
        noteStack.push(new CloseableNotes());
    }

    /**
     * Remove the instance associated with the current thread and close any pending {@link Closeable}s
     * registered with it.
     *
     * <p>
     * Callers must ensure that a matching invocation of {@link #push} has occurred previously in this thread.
     *
     * @throws IllegalStateException if there is no instance associated with the current thread
     * @throws IllegalStateException if the instance has pending {@link Closeable}s
     */
    public static void pop() {

        // Sanity check
        if (THREAD_NOTES.get() == null)
            throw new IllegalStateException("no instance associated with this thread");
        LinkedList<CloseableNotes> noteStack = THREAD_NOTES.get();
        if (noteStack.isEmpty())
            throw new IllegalStateException("no instance associated with this thread");

        // Pop off top instance and close pending closables
        noteStack.pop().closeAll();
    }

    /**
     * Add a {@link Closeable} to the list to be closed before the most recent invocation
     * of {@link #performAction performAction()} in the current thread returns.
     *
     * <p>
     * If no invocation of {@link #performAction performAction()} is executing in the current thread,
     * nothing happens (and false is returned) if {@code require} is false, otherwise an
     * {@link IllegalStateException} is thrown.
     * </p>
     *
     * @param closeable item to be added
     * @param require whether to require that {@link #performAction performAction()} is executing in the current thread
     * @return whether an invocation of {@link #performAction performAction()} is executing in the current thread
     *  and the given object was registered for closing when it completes
     * @throws IllegalArgumentException if closeable is null
     * @throws IllegalStateException if {@code require} is {@code true} but no {@link CloseableNotes} is associated
     *  with the current thread
     * @see #performAction performAction()
     */
    public static boolean addToCurrentAction(Closeable closeable, boolean require) {
        if (closeable == null)
            throw new IllegalArgumentException("null closeable");
        LinkedList<CloseableNotes> noteStack = THREAD_NOTES.get();
        if (noteStack == null || noteStack.isEmpty()) {
            if (require)
                throw new IllegalStateException("no invocation of CloseableNotes.performAction() is currently executing");
            return false;
        }
        noteStack.peek().add(closeable);
        return true;
    }
}

